package com.rtsffm.rtd.action.registry;

import com.rtsffm.rtd.action.controller.ActionDelegate;
import com.rtsffm.rtd.action.model.annotations.ActionAdopt;
import com.rtsffm.rtd.action.model.annotations.ActionChain;
import com.rtsffm.rtd.action.model.annotations.ActionFrameworkEnabled;
import com.rtsffm.rtd.action.model.annotations.ActionIdDelegatePair;
import com.rtsffm.rtd.action.model.annotations.ActionPublish;
import com.rtsffm.rtd.action.model.annotations.ActionSpecification;

//~--- JDK imports ------------------------------------------------------------



import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import static java.util.Collections.unmodifiableMap;

//~--- classes ----------------------------------------------------------------

public class ActionMetaRegistry {
    private final static Class<?>[] METHOD_SIGNATURE = new Class<?>[] { Object.class, Object.class };

    //~--- fields -------------------------------------------------------------

    private final ConcurrentMap<Class<?>, Map<String, PExecution>> processedActions = new ConcurrentHashMap<Class<?>,
                                                                                          Map<String, PExecution>>();
    private final ConcurrentMap<Class<?>, Map<String, PExecution>> processedImports = new ConcurrentHashMap<Class<?>,
                                                                                          Map<String, PExecution>>();
    private final ConcurrentMap<Class<?>, Map<Field, Map<String, ? extends Execution>>> processedFieldImports =
        new ConcurrentHashMap<Class<?>, Map<Field, Map<String, ? extends Execution>>>();
    private final ConcurrentMap<Class<?>, Map<String, PExecution>> processedActionChains =
        new ConcurrentHashMap<Class<?>, Map<String, PExecution>>();

    //~--- methods ------------------------------------------------------------

    public boolean registerMetaData(Class<?> clazz) {
        if (!clazz.isAnnotationPresent(ActionFrameworkEnabled.class)) {
            return false;
        }

        if (processedActions.containsKey(clazz)) {
            return true;
        }

        // Process declared actions on class level
        Map<String, PExecution>        actionExecutionMap = new HashMap<String, PExecution>();
        final Map<Method, ActionChain> actionChainMap     = new HashMap<Method, ActionChain>();

        for (Method m : clazz.getMethods()) {
            ActionSpecification spec;

            if (!Arrays.equals(METHOD_SIGNATURE, m.getParameterTypes())) {
                continue;
            }

            if ((spec = m.getAnnotation(ActionSpecification.class)) != null) {
                PExecution exec;

                if ((exec = actionExecutionMap.get(spec.actionId())) != null) {
                    exec.addNextExecution(newSingleExec(m));
                } else {
                    actionExecutionMap.put(spec.actionId(), new PMultipleExecution(newSingleExec(m)));
                }
            }

            ActionChain chainSpec;

            if ((chainSpec = m.getAnnotation(ActionChain.class)) != null) {
                actionChainMap.put(m, chainSpec);
            }
        }

        actionExecutionMap = putIfAbsentOrGetIfExists(processedActions, clazz, unmodifiableMap(actionExecutionMap));

        // Process adopted actions on class level
        ActionAdopt             adoptSpec;
        Map<String, PExecution> adoptedActions        = Collections.emptyMap();
        Map<String, Class<?>>   adoptedActionClassMap = Collections.emptyMap();

        if ((adoptSpec = clazz.getAnnotation(ActionAdopt.class)) != null) {
            adoptedActions = getAdoptedActions(adoptSpec, adoptedActionClassMap = new HashMap<String, Class<?>>());
        }

        adoptedActions = putIfAbsentOrGetIfExists(processedImports, clazz, adoptedActions);

        // Process chain actions on class level
        if (actionChainMap.isEmpty()) {
            Map<String, PExecution> chainExecutions = Collections.emptyMap();

            chainExecutions = putIfAbsentOrGetIfExists(processedActionChains, clazz, chainExecutions);
        } else {
            Map<String, PExecution> chainExecutions = new HashMap<String, PExecution>();

_loop:
            for (Entry<Method, ActionChain> entry : actionChainMap.entrySet()) {
                ActionChain     spec      = entry.getValue();
                PChainExecution chainExec = new PChainExecution(entry.getKey());

                for (String actionId : spec.actionIds()) {
                    PExecution pExec;

                    if ((pExec = actionExecutionMap.get(actionId)) != null) {
                        chainExec.addToChain(actionId, clazz, pExec);
                    } else if ((pExec = adoptedActions.get(actionId)) != null) {
                        Class<?> invokerClazz;

                        if ((invokerClazz = adoptedActionClassMap.get(actionId)) != null) {
                            chainExec.addToChain(actionId, invokerClazz, pExec);
                        } else {
                            continue _loop;
                        }
                    }
                }

                chainExec.addNextExecution(newSingleExec(entry.getKey()));
                chainExecutions.put(spec.chainId(), chainExec);
            }

            chainExecutions = putIfAbsentOrGetIfExists(processedActionChains, clazz, unmodifiableMap(chainExecutions));
        }


        Map<Field, Map<String, ? extends Execution>> fieldActionImports = new HashMap<Field,
                                                                              Map<String, ? extends Execution>>();
        Map<Field, Map<String, Class<?>>> fieldAdoptedClassActionMap = new HashMap<Field, Map<String, Class<?>>>();

        for (Field f : clazz.getFields()) {
            ActionAdopt spec;

            if (!Modifier.isPublic(f.getModifiers())) {
                continue;
            }

            if ((spec = f.getAnnotation(ActionAdopt.class)) != null) {
                Map<String, PExecution> fieldExecMap    = new HashMap<String, PExecution>();
                Map<String, PExecution> fieldOwnExecMap = loadAndGetActionMap(f.getType());
                Map<String, Class<?>>   fieldAdoptMap;

                fieldAdoptedClassActionMap.put(f, fieldAdoptMap = new HashMap<String, Class<?>>());
                fieldExecMap.putAll(getAdoptedActions(spec, fieldAdoptMap));

                if (fieldOwnExecMap != null) {
                    fieldExecMap.putAll(fieldOwnExecMap);
                }

                fieldActionImports.put(makeAccessible(f), Collections.unmodifiableMap(fieldExecMap));
            }
        }

        fieldActionImports = putIfAbsentOrGetIfExists(processedFieldImports, clazz,
                unmodifiableMap(fieldActionImports));

        return true;
    }

    private <K, V> V putIfAbsentOrGetIfExists(ConcurrentMap<K, V> toBeInsertedIntoMap, K key, V value) {
        final V newValue = value;

        if ((value = toBeInsertedIntoMap.putIfAbsent(key, value)) == null) {
            value = newValue;
        }

        return value;
    }

    private PExecution newSingleExec(Method m) {
        return new PSingleExecution(makeAccessible(m));
    }

    private <T extends AccessibleObject> T makeAccessible(T ao) {
        try {
            ao.setAccessible(true);
        } catch (SecurityException e) {

            /**
             * setAccessible() failed on a public accessible element
             * do not bother, may be the "suppressAccessChecks" is
             * disabled on JVM
             */
        }

        return ao;
    }

    private Map<String, PExecution> loadAndGetActionMap(Class<?> clazz) {
        if (processedActions.get(clazz) == null) {
            registerMetaData(clazz);
        }

        return processedActions.get(clazz);
    }

    //~--- get methods --------------------------------------------------------

    private Map<String, PExecution> getAdoptedActions(ActionAdopt adoptSpec,
            Map<String, Class<?>> adoptedActionIdClassMap) {
        Set<ActionIdDelegatePair> importActions   = new HashSet<ActionIdDelegatePair>(Arrays.asList(adoptSpec.adopt()));
        Map<String, PExecution>   importedActions = new HashMap<String, PExecution>();

        for (ActionIdDelegatePair pair : importActions) {
            Class<? extends ActionDelegate> delegateClass           = pair.actionImplClass();
            String[]                        actionIds               = pair.actionIds();
            Map<String, PExecution>         exportingClazzActionMap = loadAndGetActionMap(delegateClass);

            if (exportingClazzActionMap != null) {
                ActionPublish exportSpec;
                Set<String>   exportedActions = Collections.emptySet();

                if ((exportSpec = delegateClass.getAnnotation(ActionPublish.class)) != null) {
                    String[] publishedActions;

                    if ((publishedActions = exportSpec.publishedActions()).length != 0) {
                        exportedActions = new HashSet<String>(Arrays.asList(publishedActions));
                    } else {
                        exportedActions = exportingClazzActionMap.keySet();
                    }
                }

                /**
                 * // or by default we want to export all the actions irrespective
                 * // if the ActionPublish annotation??
                 * else {
                 *     exportedActions = exportingClazzActionMap.keySet();
                 * }
                 */

                for (String actionId : actionIds) {
                    if (exportedActions.contains(actionId) && exportingClazzActionMap.containsKey(actionId)
                            &&!importedActions.containsKey(actionId)) {
                        importedActions.put(actionId, exportingClazzActionMap.get(actionId));
                        adoptedActionIdClassMap.put(actionId, delegateClass);
                    }
                }
            }
        }

        return unmodifiableMap(importedActions);
    }

    public Map<String, ? extends Execution> getActionMetaMap(Class<?> clazz) {
        return processedActions.get(clazz);
    }

    public Map<String, ? extends Execution> getActionImportMetaMap(Class<?> clazz) {
        return processedImports.get(clazz);
    }

    public Map<Field, Map<String, ? extends Execution>> getActionFieldMetaMap(Class<?> clazz) {
        return processedFieldImports.get(clazz);
    }

    public Map<String, ? extends Execution> getActionChainMetaMap(Class<?> clazz) {
        return processedActionChains.get(clazz);
    }
}
